package org.ahead4.joint.utils;

import org.apache.commons.lang3.StringUtils;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * 常用签名算法
 *
 * @author WangYu
 * 2020/2/1423:36
 */
public class SignUtils {
    /**
     * 算法类型：MD5
     */
    public static final String ALGORITHM_MD5 = "MD5";
    /**
     * 算法类型：SHA1
     */
    public static final String ALGORITHM_SHA1 = "SHA1";
    /**
     * 算法类型：SHA256
     */
    public static final String ALGORITHM_SHA256 = "SHA256";
    /**
     * 算法类型：RSA
     */
    public static final String ALGORITHM_RSA = "RSA";
    /**
     * 算法类型：AES
     */
    public static final String ALGORITHM_AES = "AES";

    public static void main(String[] args) {
        // 支持的算法
        String[] algorithms = {ALGORITHM_MD5, ALGORITHM_SHA1, ALGORITHM_SHA256, ALGORITHM_RSA, ALGORITHM_AES};
        for (String algorithm : algorithms) {
            String secretKey = "123";
            String publicKey = "";
            if (ALGORITHM_RSA.equalsIgnoreCase(algorithm)) {
                // 生成RSA密钥对
                Map<String, String> RSAKeys = RSAUtils.genKeyPairBase64(1024);
                secretKey = RSAKeys.get(RSAUtils.PRIVATE_KEY);
                publicKey = RSAKeys.get(RSAUtils.PUBLIC_KEY);
            }
            Map<String, String> paraMap = new HashMap<>();
            paraMap.put("paraMap", "23");
            paraMap.put("urlEncode", "123");
            System.out.println("算法：" + algorithm + "\n签名内容：" + formatUrlMap(paraMap, false, false));
            String signStr = sign(paraMap, secretKey, algorithm);
            System.out.println("签名值:" + signStr);
            Map<String, String> duplicateParaMap = new HashMap<>();
            duplicateParaMap.put("sign", signStr);
            duplicateParaMap.putAll(paraMap);
            System.out.println("对值验签：" + verify(duplicateParaMap, ALGORITHM_RSA.equalsIgnoreCase(algorithm) ? publicKey : secretKey, algorithm));
            duplicateParaMap.put("si1gn", signStr);
            System.out.println("错值验签：" + verify(duplicateParaMap, ALGORITHM_RSA.equalsIgnoreCase(algorithm) ? publicKey : secretKey, algorithm));
        }
    }

    /**
     * 生成签名
     *
     * @param paraMap   报文
     * @param secretKey 密钥（algorithm为RSA时传入私钥）
     * @param algorithm 算法（MD5,SHA1,SHA256,RSA,AES）
     * @return 签名结果
     */
    public static String sign(Map<String, String> paraMap, String secretKey, String algorithm) {
        String str = formatUrlMap(paraMap, true, true);
        if (ALGORITHM_MD5.equalsIgnoreCase(algorithm)) {
            return MD5(Objects.requireNonNull(str));
        }
        if (ALGORITHM_SHA1.equalsIgnoreCase(algorithm)) {
            return SHA1(Objects.requireNonNull(str));
        }
        if (ALGORITHM_SHA256.equalsIgnoreCase(algorithm)) {
            return SHA256(Objects.requireNonNull(str));
        }
        if (ALGORITHM_RSA.equalsIgnoreCase(algorithm)) {
            try {
                return RSAUtils.sign(Objects.requireNonNull(str).getBytes(), secretKey);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("RSA签名失败！algorithm为RSA时secretKey请传入私钥！");
            }
        }
        if (ALGORITHM_AES.equalsIgnoreCase(algorithm)) {
            try {
                return AESUtils.Encrypt(MD5(Objects.requireNonNull(str)), secretKey);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("AES签名失败！（签名前会对原文执行MD5散列）");
            }
        }
        throw new RuntimeException("指定签名算法不存在！支持MD5,SHA1,SHA256,RSA,AES");
    }

    /**
     * 验证签名
     *
     * @param paraMap   报文
     * @param secretKey 密钥（algorithm为RSA时传入公钥）
     * @param algorithm 算法（MD5,SHA1,SHA256,RSA,AES）
     * @return 验签结果
     */
    public static boolean verify(Map<String, String> paraMap, String secretKey, String algorithm) {
        // 旧签名
        String oldSign = paraMap.get("sign");
        // 复制一份
        Map<String, String> duplicateParaMap = new HashMap<>(paraMap);
        duplicateParaMap.remove("sign");
        // 算法为RSA场合
        if (ALGORITHM_RSA.equalsIgnoreCase(algorithm)) {
            try {
                return RSAUtils.verify(Objects.requireNonNull(formatUrlMap(duplicateParaMap, true, true)).getBytes(), secretKey, oldSign);
            } catch (Exception ignored) {
            }
            return false;
        }
        // 生成签名
        String newSign = sign(duplicateParaMap, secretKey, algorithm);
        // 比较签名是否一致
        return oldSign.equalsIgnoreCase(newSign);
    }

    /**
     * 将byte转为16进制
     */
    private static String byte2Hex(byte[] bytes) {
        StringBuilder stringBuffer = new StringBuilder();
        String temp;
        for (byte aByte : bytes) {
            temp = Integer.toHexString(aByte & 0xFF);
            if (temp.length() == 1) {
                //1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }

    /**
     * 利用java原生的摘要实现SHA256加密
     *
     * @param str 加密后的报文
     */
    public static String SHA256(String str) {
        MessageDigest messageDigest;
        String encodeStr = "";
        try {
            messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
            encodeStr = byte2Hex(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return encodeStr;
    }

    /*
     * SHA1散列，结果为小写
     */
    public static String SHA1(String decript) {
        try {
            MessageDigest digest = MessageDigest
                    .getInstance("SHA-1");
            digest.update(decript.getBytes());
            byte[] messageDigest = digest.digest();
            // Create Hex String
            StringBuilder hexString = new StringBuilder();
            // 字节数组转换为 十六进制 数
            for (byte b : messageDigest) {
                String shaHex = Integer.toHexString(b & 0xFF);
                if (shaHex.length() < 2) {
                    hexString.append(0);
                }
                hexString.append(shaHex);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * MD5散列
     */
    public static String MD5(String plainText) {
        String str = "";
        try {
            MessageDigest md = MessageDigest.getInstance(ALGORITHM_MD5);
            md.update(plainText.getBytes());
            byte[] b = md.digest();

            int i;

            StringBuilder buf = new StringBuilder();
            for (byte value : b) {
                i = value;
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            str = buf.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return str;
    }

    /**
     * 按照ASCII排序签名字段并通过&连接
     *
     * @param paraMap    参数
     * @param urlEncode  是否URL转码
     * @param keyToLower 键转为小写
     */
    public static String formatUrlMap(Map<String, String> paraMap, boolean urlEncode, boolean keyToLower) {
        String buff;
        try {
            List<Map.Entry<String, String>> infoIds = new ArrayList<>(paraMap.entrySet());
            // 对所有传入参数按照字段名的 ASCII 码从小到大排序（字典序）
            infoIds.sort(Map.Entry.comparingByKey());
            // 构造URL 键值对的格式
            StringBuilder buf = new StringBuilder();
            for (Map.Entry<String, String> item : infoIds) {
                if (StringUtils.isNotBlank(item.getKey())) {
                    String key = item.getKey();
                    String val = item.getValue();
                    if (urlEncode) {
                        val = URLEncoder.encode(val, "utf-8");
                    }
                    if (keyToLower) {
                        buf.append(key.toLowerCase()).append("=").append(val);
                    } else {
                        buf.append(key).append("=").append(val);
                    }
                    buf.append("&");
                }

            }
            buff = buf.toString();
            if (!buff.isEmpty()) {
                buff = buff.substring(0, buff.length() - 1);
            }
        } catch (Exception e) {
            return null;
        }
        return buff;
    }
}
